package transcoding
import controllers.LibraryManager
import com.xuggle.xuggler.IContainerFormat
import java.io.InputStream
import play.api.Logger
import java.util.concurrent.atomic.AtomicBoolean
import scala.actors.Actor
import java.io.BufferedReader
import java.io.InputStreamReader
import java.util.concurrent.ConcurrentHashMap
import scala.collection.immutable.HashSet

object TranscodeProcess {
  val ffmpeg = "ffmpeg"
  val transcodingCmds = Map(
    "video-alsowrite" -> (ffmpeg + " -y -ss 0 -i $file -async 1 -b $bitrate -s $size -ar 44100 -ac 2 -v 1 -f flv -vcodec libx264 -preset superfast -threads 0 - -f $format -ar 44100 -ac 2 -vcodec libx264 -threads 0 -metadata grouping=$file -metadata comment=oktostream $outFile"),
    "video-skipto" -> (ffmpeg + " -ss $offset -i $file -v 1 -async 1 -f flv -vcodec copy -acodec copy -preset superfast -threads 0 -"),
    "video-makeskippable" -> "qt-faststart $source $dest")

  /**
   * Creates a TranscodeProcess that when executed
   * will simply copy the file from the offset specified
   */
  def startSkipTo(mediaFile: MediaFile, offsetSeconds: Long)(implicit pi: ProcessInfo) = {

    // TODO: how to detect programmatically 
    // if the moov atom is at the beginning of the file?
    val isSkippable = false //mediaFile.format.map(f => f.getInputFormatShortName().contains("mp4")).getOrElse(false)

    if (isSkippable) {
      val fis = new java.io.FileInputStream(mediaFile.filePath)
      new CopyStreamTranscodeProcess(TranscoderUtil.forceSkip(fis, 0))
    } else {
      val cmd = mediaFile.mediaType match {
        case v @ Video =>
          transcodingCmds.get("video-skipto").map(
            cmd => cmd.replace("$offset", offsetSeconds.toString)
              .replace("$file", TranscoderUtil.encodeSpaces(mediaFile.filePath))) match {
              case None => throw new RuntimeException("Could not find relevant command for '" + v + "/skipto'")
              case Some(toExec) => toExec
            }
      }

      new SingleCmdTranscodeProcess(cmd)
    }
  }

  def startRaw(mediaFile: MediaFile)(implicit pi: ProcessInfo) = {
    new CopyStreamTranscodeProcess(new java.io.FileInputStream(mediaFile.filePath))
  }

  def startTranscodingWithSave(mf: MediaFile, maxBitRate: Option[Int], format: Option[String])(implicit pi: ProcessInfo) = {
    mf.mediaType match {
      case v @ Video =>
        val extension = format.getOrElse("mp4")
        val outFile = mf.filePath.replace(LibraryManager.getLibraryDir, LibraryManager.getAltLibraryDir)
          .+("_bladeVersion." + extension + ".PART")

        transcodingCmds.get("video-alsowrite").map(
          cmd => {
            cmd.replace("$file", TranscoderUtil.encodeSpaces(mf.filePath))
              .replace("$bitrate", maxBitRate.getOrElse(mf.bitRate.getOrElse(1000000)).toString)
              .replace("$size", TranscoderUtil.size(maxBitRate)(mf))
              .replace("$format", extension)
              .replace("$outFile", TranscoderUtil.encodeSpaces(TranscoderUtil.ensureDirExists(outFile)))
          }) match {
            case None => throw new RuntimeException("Could not find relevant command for '" + v + "/alsowrite'")
            case Some(toExec) =>
              toExec
              new TranscodingAndSavingTranscodeProcess(toExec, mf, outFile)
          }
    }
  }

  def startTranscodingAdHoc = { /*TODO*/ }
  def startTranscodingForSave = { /*TODO*/ }

}

abstract class TranscodeProcess(implicit val procInfo: ProcessInfo) {
  var started:Long = 0
  def isActive {}
  def destroy {}
  def getInputStream: TranscodedInputStream
  def getListener: InputStreamListener
  def isCancellable: Boolean

  protected def newPorcessBuilder(cmd: String) = {
    val argsPairs = new java.util.ArrayList[String]()
    cmd.split("\\s+").map(e => argsPairs add TranscoderUtil.decodeSpaces(e))

    new ProcessBuilder(argsPairs)
  }
  
  protected def startProcess(pb: ProcessBuilder) = {
    Logger.debug("Running command: " + pb.command)
    val p = pb.start
    started = System.currentTimeMillis
    val procIS: InputStream = p.getInputStream()

    val streamGobbler = new StreamGobbler
    streamGobbler.start()
    streamGobbler ! ('AddStream, p.getErrorStream())
    TranscodeProcessManager.registerProcessAsActive(this)

    p
  }

  protected def unregister = TranscodeProcessManager.unregisterProcess(this)
  override def hashCode = {
    procInfo.PID.hashCode
  }
  override def equals(other: Any) = {
    if (other != null)
      other match {
        case o: TranscodeProcess =>
          procInfo.PID == procInfo.PID
        case _ => false
      }
    else false
  }
}

class TranscodingAndSavingTranscodeProcess(cmd: String, mf: MediaFile, savingFilePath: String)(implicit pi: ProcessInfo) extends SingleCmdTranscodeProcess(cmd) {

  private val destFilePath = savingFilePath.replaceAll("\\.PART$", "")
  class KeepAliveTranscodingPorcess extends Actor {
    def act = {
      loop {
        react {
          case ('KeepAlive, p: java.lang.Process, savingFilePath: String, callback: ((Option[Throwable]) => Unit)) =>

            Logger.debug("[%s]Keeping alive transcoding process...".format(this.toString))
            Logger.debug("Will postprocess " + savingFilePath)

            var reader: BufferedReader = null
            try {
              reader = new BufferedReader(new InputStreamReader(p.getInputStream))

              def read: Unit = {
                val line = reader.readLine()
                if (line != null)
                  read
              }
              read

              if (0 == p.waitFor()) {
                Logger.info("START post processing ...")

                val cmd = TranscodeProcess.transcodingCmds.get("video-makeskippable")
                  .getOrElse("")
                  .replace("$source", TranscoderUtil.encodeSpaces(savingFilePath))
                  .replace("$dest", TranscoderUtil.encodeSpaces(destFilePath))

                val postProcessCmd = startProcess(newPorcessBuilder(cmd))
                if (0 == postProcessCmd.waitFor()) {
                  if (!new java.io.File(destFilePath).exists)
                    throw new RuntimeException("Could not find expected post-processed file at " + destFilePath)
                  if (!new java.io.File(savingFilePath).delete())
                    Logger.warn("Could not delete temp file " + savingFilePath)

                  Logger.info("... DONE post processing")
                } else throw new RuntimeException("PostProcessCmd (%s) returned a non-zero exit code".format(postProcessCmd))
              } else throw new RuntimeException("Transcoding process returned a non-zero exit code while dealing with file %s"
                .format(pi.mfm.mainMediaFile.filePath))

              callback(None)

            } catch {
              case error =>
                Logger.error("Error while keeping alive and registering saved transcoded file", error)
                callback(Some(error))
            } finally {
              TranscoderUtil.closeQuietly(reader)
              if (p != null) p.destroy
            }
          case msg => Logger.error("Incorrect message received %s".format(msg.toString))
        }
      }
    }
  }

  private val isInBackground = new AtomicBoolean(false)
  override def isCancellable = isInBackground.get

  override def getListener = new InputStreamListener {

    def onDone(maybeError: Option[Throwable]) = {
      p.exitValue
      maybeError match {
        case None =>
          Logger.debug("Registering saved file as alternative of " + mf.filePath)
          unregister
          LibraryManager.addAlternativeFormat(mf, new java.io.File(destFilePath))
      }

      ()
    }
    override def onCloseRequest(is: InputStream) = {

      Logger.debug("Setting transcode process as cancellable but keeping git alive")
      isInBackground.set(true)

      val keepAlive = new KeepAliveTranscodingPorcess
      keepAlive.start ! ('KeepAlive, p, mf, savingFilePath, onDone _) //(a) => this.onDone(a)
      //TranscoderUtil.closeQuietly(is)
    }
  }
}

class SkippedStreamTranscodeProcess(cmd: String)(implicit pi: ProcessInfo) extends TranscodeProcess {
  override def getListener = new InputStreamListener {
    override def onCloseRequest(is: InputStream) = {
      TranscoderUtil.closeQuietly(is)
      unregister
    }
  }
  val p = startProcess(newPorcessBuilder(cmd))
  val is = new TranscodedInputStream(p.getInputStream, getListener)

  override def isCancellable = false
  override def getInputStream = is
  override def isActive = {
    try { if (p == null) false else { p.exitValue; true } }
    catch { case _ => false }
  }
  override def destroy = {
    destroy(p)
  }
}

class CopyStreamTranscodeProcess(val adapteeInputStream: InputStream)(implicit pi: ProcessInfo) extends TranscodeProcess {
  val isClosed = new AtomicBoolean(false)
  override def getListener = new InputStreamListener {
    override def onCloseRequest(is: InputStream) = {
      TranscoderUtil.closeQuietly(is)
      isClosed.set(true)
      unregister
    }
  }
  val is = new TranscodedInputStream(adapteeInputStream, getListener)
  started = System.currentTimeMillis
  TranscodeProcessManager.registerProcessAsActive(this)

  override def isCancellable = false
  override def getInputStream = is
  override def isActive = {
    !isClosed.get
  }
  override def destroy = {
    try { adapteeInputStream.close }
    catch { case _ => }
  }
}

class ProcessInfo(val PID: String, val destination: String, val mfm: MediaFileManager) {
  override def toString: String = "[%s] - %s".format(PID, destination)
}

object TranscodeProcessManager {

  val processesMap = new ConcurrentHashMap[String, Set[TranscodeProcess]]

  /**
   * Tell the manager that this process is now active
   */
  def registerProcess(tp: TranscodeProcess) = {
    val key = tp.procInfo.mfm.mainMediaFile.filePath 
    val processes = ifNotNull(processesMap.get(key)){
    	processesForFile =>  Some(processesForFile + tp)
    }.getOrElse(new HashSet + tp)
    processesMap.put(key, processes)
    Logger.debug("Registered " + tp)
  }
  /**
   * Tell the manager that this process is now  no longer active
   */
  def unregisterProcess(tp: TranscodeProcess) = {
    processesMap.remove(tp.procInfo.PID)
    Logger.debug("Unregistered " + tp)
  }

  def getTrancodingVideoStream(pi: ProcessInfo, mediaID: String, maxBitRate: Option[Int], format: Option[String], offset: Long) = {
    LibraryManager.getMediaLibrary.map(
      library => {
        ifNotNull(library.get(Video)) {
          libraryForType =>
            ifNotNull(libraryForType.get(mediaID)) {
              mfm => startProcessAndContructStream(pi, mfm, maxBitRate, format, offset)
            }
        }
      }).getOrElse(None)
  }

  def startProcessAndContructStream(pi: ProcessInfo, mfm: MediaFileManager, maxBitRate: Option[Int], format: Option[String], offset: Long): Option[(InputStream, MediaFile)] = {
    val requiredFormat = getFormat(format)

    implicit val procInfo = pi
    Logger.debug("FORMAT REQUIRED " + requiredFormat)
    Logger.warn(mfm.otherVersions.toString)
    mfm.getMediaFileWithMaxBitRateAndFormat(maxBitRate, requiredFormat) match {
      case Some(mf) => {
        Logger.debug("FOUND FORMAT " + mf.format)
        // Matching Media FIle
        // => no transcoding required
        offset match {
          case 0 =>
            // No skipping requested
            // => send pure FileInputStream
            Logger.debug("TranscodeProcess.startRaw")
            Some((TranscodeProcess.startRaw(mf).getInputStream), mf)
          case secondOffset =>
            // Use ffmpeg with 'copy' encoder to skip
            // to the relevant portion of the stream
            Logger.debug("TranscodeProcess.startSkipTo with %d seconds offset".format(secondOffset))
            Some((TranscodeProcess.startSkipTo(mf, secondOffset).getInputStream), mf)
        }
      }
      case None => {
        // Transcoding required, and if the bitrate/format 
        // is one of the standard one, we will track this
        // ffmpeg execution, which will also produce a regular file
        // and add this file to the alternative available to this MediaFileManager
        Logger.debug("TranscodeProcess.startTranscoding ...")

        Some(TranscodeProcess.startTranscodingWithSave(mfm.mainMediaFile, maxBitRate, format).getInputStream, mfm.mainMediaFile)
      }
    }
  }

  private def getFormat(maybeFormat: Option[String]) = {
    maybeFormat.map(
      format =>
        {
          val contFormat = IContainerFormat.make()
          contFormat.setInputFormat(format);

          contFormat
        })
  }

  private def ifNotNull[A, B](value: B)(f: B => Option[A]) = {
    if (null == value) None
    else f(value)
  }
}